package com.itheima.admin.service.impl;

import com.alibaba.fastjson.JSON;
import com.hankcs.algorithm.AhoCorasickDoubleArrayTrie;
import com.itheima.admin.mapper.AdChannelMapper;
import com.itheima.admin.mapper.AdSensitiveMapper;
import com.itheima.admin.service.WemediaNewsAutoScanService;
import com.itheima.article.dto.ArticleInfoDto;
import com.itheima.article.feign.ApArticleFeign;
import com.itheima.article.pojo.ApArticle;
import com.itheima.article.pojo.ApArticleConfig;
import com.itheima.article.pojo.ApArticleContent;
import com.itheima.common.constants.BusinessConstants;
import com.itheima.common.exception.LeadNewsException;
import com.itheima.common.util.SensitiveWordUtil;
import com.itheima.common.util.ali.AliGreenScanner;
import com.itheima.common.vo.ResultVo;
import com.itheima.dfs.feign.DfsFeign;
import com.itheima.wemedia.dto.WmNewsContentNodeDto;
import com.itheima.wemedia.dto.WmNewsPageReqDto;
import com.itheima.wemedia.feign.WmNewsFeign;
import com.itheima.wemedia.pojo.WmNews;
import com.itheima.wemedia.vo.WmNewsVo;
import com.xxl.job.core.log.XxlJobLogger;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @version 1.0
 * @description 说明
 * @package com.itheima.admin.service.impl
 */
@Service
@Slf4j
public class WemediaNewsAutoScanServiceImpl implements WemediaNewsAutoScanService {

    @Resource
    private WmNewsFeign wmNewsFeign;

    @Resource
    private DfsFeign dfsFeign;

    @Resource
    private AdSensitiveMapper adSensitiveMapper;

    @Resource
    private AliGreenScanner aliGreenScanner;

    @Resource
    private AdChannelMapper adChannelMapper;

    @Resource
    private ApArticleFeign apArticleFeign;

    /**
     * 文章自动审核
     *
     * @param uuid
     * @param wmNewsId
     */
    @Override
    public void autoScanWemediaNewsById(String uuid, long wmNewsId) {
        log.debug("进行远程调用查询文章信息:{},{}",uuid,wmNewsId);
        ResultVo<WmNews> getWmNewsResult = wmNewsFeign.getWmNewsById(wmNewsId);
        if(!getWmNewsResult.isSuccess()){
            log.error("远程调用查询文章信息失败: {},{}", uuid,getWmNewsResult.getErrorMessage());
            //TODO 标记需要人工审核  其实不必这样处理，直接报错就好了。由运维人员来处理
            throw new LeadNewsException("远程调用查询文章信息失败");
        }
        // 得到文章
        WmNews wmNews = getWmNewsResult.getData();
        // 非空判断
        if(null == wmNews){
            // 由运维人员来处理
            throw new LeadNewsException("查询不到文章信息!");
        }
        // 状态判断 幂等处理
        log.info("文章状态: {},{}", uuid,wmNews.getStatus());
        if(wmNews.getStatus() != BusinessConstants.WmNewsConstants.STATUS_SUBMIT){
            // 只有待审核的才处理，不为1，不是待审核状态，不处理
            log.debug("文章状态为1，不处理 {}",uuid);
            return;
        }
        // 提取文本
        String text = getTextFromWmNews(wmNews);
        // 敏感词检测
        boolean hasSensitiveWords = sensitiveWordsCheck(text);
        log.info("敏感词检测结果为：{},{}", uuid,hasSensitiveWords);
        // 更新文章对象
        WmNews updatePojo = new WmNews();
        updatePojo.setId(wmNewsId);

        if(hasSensitiveWords){
            log.debug("文章包含敏感词!{}",uuid);
            // 更新文章状态为 审核失败，reason=包含敏感词
            updatePojo.setStatus(BusinessConstants.WmNewsConstants.STATUS_FAIL);
            updatePojo.setReason("文章包含敏感词");
        }else{
            // 阿里文本反垃圾处理
            try {
                Map<String, String> scanResult = aliGreenScanner.greenTextScan(text);
                log.info("阿里文本反垃圾结果: {},{}", uuid,scanResult);
                if(null != scanResult && "pass".equals(scanResult.get("suggestion"))){
                    // 文本反垃圾通过了
                    // 提取文章中的图片
                    Set<String> imageUrls = getImageFromWmNews(wmNews);
                    log.info("需要审核的图片共有{}张.{}", imageUrls.size(), uuid);
                    if(!CollectionUtils.isEmpty(imageUrls)) {
                        scanResult = null;
                        // 调用dfs下载图片
                        ResultVo<List<byte[]>> downloadResult = dfsFeign.download(imageUrls);
                        if(!downloadResult.isSuccess()){
                            throw new LeadNewsException("文件微服下载图片失败!");
                        }
                        // 阿里图片反垃圾
                        List<byte[]> imageList = downloadResult.getData();
                        scanResult = aliGreenScanner.imageScan(imageList);
                        log.info("阿里图片反垃圾结果: {},{}", uuid,scanResult);
                    }
                }
                // 对阿里反垃圾统一处理
                String suggestion = scanResult.get("suggestion");
                switch (suggestion){
                    case "pass": updatePojo.setStatus(BusinessConstants.WmNewsConstants.STATUS_PASS);break;
                    case "block": updatePojo.setStatus(BusinessConstants.WmNewsConstants.STATUS_FAIL);
                        updatePojo.setReason(scanResult.get("label"));break;
                    case "review": updatePojo.setStatus(BusinessConstants.WmNewsConstants.STATUS_MANUAL);break;
                }
            } catch (Exception e) {
                if(e instanceof LeadNewsException){
                    throw (LeadNewsException)e;
                }else{
                    log.error("调用阿里反垃圾失败!{}",uuid, e);
                    throw new LeadNewsException("调用阿里反垃圾失败!");
                }
            }
        }
        // 更新文章的状态
        log.info("更新文章状态为: {},{}", uuid,updatePojo);
        ResultVo updateResult = wmNewsFeign.updateWmNews(updatePojo);
        if(!updateResult.isSuccess()){
            log.error("调用自媒体微服更新文章状态失败!{}",uuid, updateResult.getErrorMessage());
            throw new LeadNewsException("调用自媒体微服更新文章状态失败!uuid=" + uuid);
        }
    }

    /**
     * 自媒体文章同步到微服务
     *
     * @param param
     * @param uuid
     */
    @Override
    public void syncArticles(String param, String uuid) {
        // 远程调用自媒体微服查询 文章状态为4或8的文章
        // 预留条件  为了以后功能扩展性。
        WmNewsPageReqDto pageReqDto = new WmNewsPageReqDto();
        // 发布时间要小等于当前系统时间
        pageReqDto.setEndPubDate(new Date());
        // 远程调用自媒体查询需要同步的文章
        ResultVo<List<WmNewsVo>> listResultVo = wmNewsFeign.list4ArticleSync(pageReqDto);
        if(listResultVo.isSuccess()) {
            // 获取需要同步的文章
            List<WmNewsVo> voList = listResultVo.getData();
            log.info("需要同步的文章数: {},{}", uuid, voList.size());
            if (!CollectionUtils.isEmpty(voList)) {
                // 遍历需要同步的文章
                for (WmNewsVo vo : voList) {
                    // 构建dto
                    ArticleInfoDto dto = buildArticleInfoDto(vo);
                    // 远程调用文章微同步
                    ResultVo resultVo = apArticleFeign.saveArticleInfo(dto);
                    if(!resultVo.isSuccess()){
                        XxlJobLogger.log("文章同步失败:{},{}",uuid,vo.getId());
                        // 通知运维人员: 发消息给MQ, 给运维人员
                        log.error("文章同步失败:{},{}",uuid,vo.getId());
                    }else {
                        log.info("文章{}同步完成!{}", vo.getId(), uuid);
                    }
                }
            }
        }else{
            log.error("查询需要同步的文件失败!{},{}", uuid, listResultVo.getErrorMessage());
            throw new LeadNewsException("查询需要同步的文件失败!"+uuid+"," +listResultVo.getErrorMessage());
        }
    }

    /**
     * 构建 文章 同步 对象
     * @param vo
     * @return
     */
    private ArticleInfoDto buildArticleInfoDto(WmNewsVo vo) {
        ArticleInfoDto dto = new ArticleInfoDto();
        //1. ApArticle
        ApArticle apArticle = buildApArticle(vo);
        //2. ApArticleContent
        ApArticleContent apArticleContent = buildApArticleContent(vo);
        //3. ApArticleConfig
        ApArticleConfig apArticleConfig = buildApArticleConfig(vo);
        dto.setApArticle(apArticle);
        dto.setApArticleContent(apArticleContent);
        dto.setApArticleConfig(apArticleConfig);
        // 为了在文章微中，同步完数据后，要更新自媒体wm_news的状态等...
        dto.setWmNewsId(vo.getId());
        return dto;
    }

    /**
     * 构建文章微服中的文章配置数据
     * @param vo
     * @return
     */
    private ApArticleConfig buildApArticleConfig(WmNewsVo vo) {
        ApArticleConfig apArticleConfig = new ApArticleConfig();
        // wm_news.enable 1上架，0:下架
        apArticleConfig.setIsDown(vo.getEnable()==1?0:1); // 是否下架, 1:下架，0:上架
        apArticleConfig.setIsDelete(0);// 0 未删除
        apArticleConfig.setIsComment(1);// 是否允许评论 1：可以评论
        apArticleConfig.setIsForward(1);// 是否允许转发: 1: 可以转发
        return apArticleConfig;
    }

    /**
     * 构建文章微服中的文章内容数据
     * @param vo
     * @return
     */
    private ApArticleContent buildApArticleContent(WmNewsVo vo) {
        ApArticleContent content = new ApArticleContent();
        content.setContent(vo.getContent());
        return content;
    }

    /**
     * 构建文章微服中的文章表数据
     * @param vo
     * @return
     */
    private ApArticle buildApArticle(WmNewsVo vo) {
        // 创建pojo
        ApArticle apArticle = new ApArticle();
        // 给属性赋值
        BeanUtils.copyProperties(vo,apArticle);
        apArticle.setId(null);//TODO 设置回上次的文章微服中的文章id， 文章再次发布时
        apArticle.setAuthorId(vo.getUserId());
        apArticle.setChannelName(getChannelName(apArticle.getChannelId()));
        apArticle.setLayout(vo.getType());
        apArticle.setFlag(BusinessConstants.ArticleConstants.FLAG_NORMAL);
        apArticle.setLikes(0);
        apArticle.setComment(0);
        apArticle.setCollection(0);
        apArticle.setViews(0);
        apArticle.setSyncStatus(0);// 未同步给elasticsearch
        apArticle.setOrigin(BusinessConstants.ArticleConstants.ORIGIN_WEMEDIA);
        // 返回
        return apArticle;
    }

    /**
     * 通过频道id获取频道名称
     * @param channelId
     * @return
     */
    private String getChannelName(Integer channelId) {
        // 为了方便，直接从数据库查询
        //【推荐】: 同学们有时间，优先从redis中查询
        return adChannelMapper.selectById(channelId).getName();
    }

    /**
     * 提取文章中的图片
     * @param wmNews
     * @return
     */
    private Set<String> getImageFromWmNews(WmNews wmNews) {
        Set<String> imageSet = new HashSet<>();
        // 封面+内容中的图片
        String images = wmNews.getImages();
        if(!StringUtils.isEmpty(images)){
            // 封面的图片是以逗号分割拼接的
            String[] covers = images.split(",");
            // 把封面加到图片合里
            Collections.addAll(imageSet, covers);
        }
        // 内容中的图片
        String contentJson = wmNews.getContent();
        List<WmNewsContentNodeDto> nodeList = JSON.parseArray(contentJson, WmNewsContentNodeDto.class);
        // 过滤图片，只保留文本,转成字符串
        List<String> contentImages = nodeList.stream().filter(node -> BusinessConstants.WmNewsConstants.CONTENT_IMAGE.equals(node.getType()))
                // 只时过滤后，流中的数据类型依然是WmNewsContentNodeDto
                // 转成字符串
                .map(WmNewsContentNodeDto::getValue).collect(Collectors.toList());
        if(!CollectionUtils.isEmpty(contentImages)){
            // 内容中有图片
            imageSet.addAll(contentImages);
        }
        return imageSet;
    }

    /**
     * 敏感词检测
     * @param text
     * @return
    private boolean sensitiveWordsCheck(String text) {
        //1. 查询数据库中的所有敏感词
        List<String> wordsList = adSensitiveMapper.selectSensitives();
        //2. 敏感词DFA初始化
        SensitiveWordUtil.initMap(wordsList);
        //3. 检测, 如果size>0说明有敏感词
        return SensitiveWordUtil.matchWords(text).size() > 0;
    }*/
    /**
     * 敏感词检测
     * @param text
     * @return
     */
    private boolean sensitiveWordsCheck(String text) {
        //1. 查询数据库中的所有敏感词
        List<String> wordsList = adSensitiveMapper.selectSensitives();
        Map<String, String> map = wordsList.stream().collect(Collectors.toMap(Function.identity(), Function.identity()));
        //2. 使用双向字典树
        AhoCorasickDoubleArrayTrie<String> acdat = new AhoCorasickDoubleArrayTrie<String>();
        acdat.build(map);
        //3. 检测
        //return acdat.findFirst(text)!=null;有敏感词
        return acdat.matches(text);//有敏感词
    }

    /**
     * 提取文章中的文本 标题+内容
     * @param wmNews
     * @return
     */
    private String getTextFromWmNews(WmNews wmNews) {
        String title = wmNews.getTitle();
        // 文章中的内容部分是包含了图片
        // 提取文本内容中的文本部分
        String contentJson = wmNews.getContent();
        List<WmNewsContentNodeDto> nodeList = JSON.parseArray(contentJson, WmNewsContentNodeDto.class);
        // 过滤图片，只保留文本,转成字符串
        String content = nodeList.stream().filter(node -> !BusinessConstants.WmNewsConstants.CONTENT_IMAGE.equals(node.getType()))
                // 只时过滤后，流中的数据类型依然是WmNewsContentNodeDto
                // 转成字符串
                .map(WmNewsContentNodeDto::getValue)
                // 流里的每个元素类型已经是字符串，集合状态，而我们需要的是一个字符串
                // 把以逗号分割方式连接起来，就把流中所有的字符串拼接成一个字符串了
                .collect(Collectors.joining(","));
        // 标题最后2字为 结冰+毒 结冰毒 造成敏感词，这是错的，必须使用标点符号把标题与内容分开
        return title + "," + content;
    }
}
